﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using NHibernate;

namespace Akronus.TaskScheduler.NhbDataService
{
    public class SessionManager : IDisposable
    {
        /// <summary>
        /// A dictionary that maps root managers to the connection strings.
        /// </summary>
        [ThreadStatic]
        private static Dictionary<string, RootSessionManager> roots;

        /// <summary>
        /// The instance variable for the root manager for quick access.
        /// </summary>
        private RootSessionManager root;

        /// Indicates if current manager's Commit() method was called. 
        /// If not, the root manager will roll back the transaction if it exists.
        /// This field is used when the object is being disposed.
        private bool isCommited;

        /// this manager does not perform any write operations, so if true the developer can skip the Commit() call
        private bool isReadOnly;

        /// <summary>
        /// Constructs and initializes the Manager. Creates a new Session or re-uses a Session created earlier in this request.
        /// </summary>
        /// <param name="connectionString">The connection string to use for this session.</param>
        /// <param name="readOnly">If set to <see langword="true"/> no actions that are executed through current manager should perform any write operations.</param>
        public SessionManager(string connectionString, bool readOnly)
        {
            this.isReadOnly = readOnly;
            if (!InitializeAsRoot(connectionString)) // if this is a nested manager
            {
                if (!this.isReadOnly && root.Owner.isReadOnly)
                    throw new ConfigurationErrorsException("The root manager is read-only. Nested managers must also be read-only.");
            }
        }

        /// <summary>
        /// Tries to initialize the current manager as root if no one exists.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <returns>Returns the value indicating whether current manager is root.</returns>
        private bool InitializeAsRoot(string connectionString)
        {
            if (roots == null)
                roots = new Dictionary<string, RootSessionManager>();
            else if (roots.TryGetValue(connectionString, out root))
                return false;
            root = new RootSessionManager(this, new SessionCreator(), connectionString);
            if (!this.isReadOnly)
                root.Session.Transaction.Begin();
            roots[connectionString] = root;
            return true;
        }

        /// <summary>
        /// Returns the current NHibernate session. Use this to access the data.
        /// </summary>
        public ISession Session
        {
            get { return this.root.Session; }
        }

        /// <summary>
        /// Commits pending actions. This method should be called on every non read-only session manager in order to commit its changes.
        /// If at least one is not commited the whole transaction is rolled back.
        /// Read-only session managers do not require to call this method.
        /// </summary>
        public void Commit()
        {
            this.isCommited = true;
        }

        /// <summary>
        /// Rollbacks pending changes. If at least one session manager is rolled back the whole transaction is rolled back as well.
        /// This method is provided if you want to roll back the transaction explicitly.
        /// </summary>
        public void Rollback()
        {
            this.root.IsRolledBack = true;
        }

        #region IDisposable Members

        /// <summary>
        /// Flushes session and rolls uncommitted changes back.
        /// </summary>
        public void Dispose()
        {
            if (!this.isReadOnly && !this.isCommited)
                this.Rollback();

            GC.SuppressFinalize(this);
            if (root.Owner == this)
            {
                roots.Remove(root.ConnectionString);
                root.Dispose();
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposeAll"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
        public void Dispose(bool disposeAll)
        {
            this.Dispose();
        }

        #endregion

        #region Nested Types

        /// <summary>
        /// A class describing a root session manager.
        /// </summary>
        private class RootSessionManager : IDisposable
        {
            /// <summary>
            /// The owner manager that caused this instance to be created.
            /// </summary>
            public SessionManager Owner;
            /// <summary>
            /// The connection string.
            /// </summary>
            public string ConnectionString;
            /// <summary>
            /// The NHibernate session used to access the data.
            /// </summary>
            public ISession Session;

            /// <summary>
            /// If any nested manager has not been commited it sets this field of the root manager to True.
            /// </summary>
            public bool IsRolledBack;

            /// <summary>
            /// Initializes a new instance of the <see cref="RootSessionManager"/> class.
            /// </summary>
            /// <param name="owner">The owner manager that caused this instance to be created.</param>
            /// <param name="connectionString">The connection string.</param>
            public RootSessionManager(SessionManager owner, SessionCreator sessionCreator, string connectionString)
            {
                this.Owner = owner;
                this.ConnectionString = connectionString;
                this.Session = sessionCreator.Create(connectionString);
            }

            #region IDisposable Members

            /// <summary>
            /// Commits or rolls back the transaction and closes the session.
            /// </summary>
            public void Dispose()
            {
                GC.SuppressFinalize(this);
                try
                {
                    if (!Owner.isReadOnly)
                    {
                        if (this.IsRolledBack)
                            this.Session.Transaction.Rollback();
                        else
                            this.Session.Transaction.Commit();
                    }
                    // this.Session.Flush();
                }
                finally
                {
                    this.Session.Clear();
                    this.Session.Close();
                    this.Session.Dispose();
                }
            }

            #endregion
        }

        #endregion
    }
}
